<?php
/*********************************************
 *  CPG Dragonfly™ CMS
 *********************************************
	Copyright © since 2010 by CPG-Nuke Dev Team
	http://dragonflycms.org

	Dragonfly is released under the terms and conditions
	of the GNU GPL version 2 or any later version
*/

namespace Dragonfly;

abstract class Session extends \Poodle\Events
{
	public $sess_id, $dbupdate; // old code compatibility

	protected static
		$started = false;
	protected
		$mode = 0,
		$old_handler,
		$timeout = 10800, # 3 hours
		$client_ip;

	public static function factory($config)
	{
		$class = $config->handler;
		return new $class();
	}

	function __construct()
	{
		if (self::$started) { throw new \Exception('Session already started!'); }
		$this->client_ip = \Dragonfly_Net::ip();
		if (isset($_SESSION) || \Poodle\PHP\INI::get('session.auto_start'))
		{
			session_destroy();
			session_write_close();
		}
		\Poodle\PHP\INI::set('session.gc_divisor', 100);
		\Poodle\PHP\INI::set('session.gc_probability', 1);
		\Poodle\PHP\INI::set('session.gc_maxlifetime', 1440); # seconds
		# Modify PHP configuration to prevent:
		\Poodle\PHP\INI::set('session.use_only_cookies', 1); # SID in url
		\Poodle\PHP\INI::set('session.use_trans_sid', 0);    # SID ob
		\Poodle\PHP\INI::set('url_rewriter.tags', '');       # SID in tags
		$this->old_handler = \Poodle\PHP\INI::get('session.save_handler');
		$K = \Dragonfly::getKernel();
		$name = DF_SESSION_NAME;
		if (!empty($K->CFG->session->timeout)) { $this->setTimeout($K->CFG->session->timeout); }
		# Start session
		session_name($name);
		if ($K) { session_set_cookie_params(0, $K->base_uri, $K->cookie_domain); } // [, bool secure], httponly = useless due to XMLHTTPRequest
//		if ($K) { session_set_cookie_params(0, $K->CFG->cookie->path, $K->CFG->cookie->domain, ('https' === DOMAIN_PROTOCOL)); } // httponly = useless due to XMLHTTPRequest

		$this->start(isset($_COOKIE[$name]) ? $_COOKIE[$name] : null);

		if (empty($_SESSION['CPG_SESS']) && isset($_COOKIE[$name]) && session_id() == $_COOKIE[$name]) {
			$sid = $_COOKIE[$name];
			$this->destroy();
			$this->start();
			if (session_id() == $sid) {
				$this->destroy();
				cpg_error('Your cookie has expired, the page will be refreshed to set a new cookie.', 'Cookie expired', \URL::uri());
			}
		}

		# Session hijack attempt?
		if (!empty($_SESSION['_PID']) && self::pid() !== $_SESSION['_PID'])
		{
			$this->destroy();
			cpg_error('Your cookie is invalid, the page will be refreshed to set a new cookie.', 'Cookie expired', \URL::uri());
		}

		# Session expired?
		if (!$_SESSION && isset($_COOKIE[$name]) && session_id() === $_COOKIE[$name])
		{
			$this->start();
		}

		if (!$this->mode && !$_SESSION) $this->mode = 1;

		// old code compatibility
		$this->new = $this->is_new();
		$this->sess_id = session_id();

		$_SESSION['CPG_SESS'] = empty($_SESSION['CPG_SESS']) ? array() : $_SESSION['CPG_SESS'];
		$GLOBALS['CPG_SESS'] = $_SESSION['CPG_SESS'];

		if (empty($_SESSION['DF_VISITOR']) || !($_SESSION['DF_VISITOR'] instanceof \Dragonfly_Session_Visitor))
		{
			$_SESSION['DF_VISITOR'] = new \Dragonfly_Session_Visitor();
		}
	}

	function __destruct()
	{
		self::close();
		if ($this->old_handler) { \Poodle\PHP\INI::set('session.save_handler', $this->old_handler); }
	}

	abstract protected function setHandler();
	protected function after_start($id)
	{
		$K = \Dragonfly::getKernel();
		if ($K && $K->SQL && isset($K->SQL->TBL->sessions)) {
			$value = $K->SQL->uFetchRow('SELECT sess_reset FROM '.$K->SQL->TBL->sessions.' WHERE sess_id='.$K->SQL->quote($id).' AND sess_expiry > '.time());
			if (!empty($value) && $value[0]) { $this->mode = 2; }
		}
		$this->setTimeout(\Poodle\PHP\INI::get('session.gc_maxlifetime'));
//		if (1 === mt_rand(1, intval(\Poodle\PHP\INI::get('session.gc_divisor') / \Poodle\PHP\INI::get('session.gc_probability'))))
		if (1 === mt_rand(1, 100)) $this->db_gc();
	}
	protected function before_write_close() { return $this->db_write(session_id()); }

	public function is_new()  { return (1===$this->mode); }
	public function reset()   { return (2===$this->mode); }
	public function timeout() { return $this->timeout; }
	public function setTimeout($time) {
		$time = intval($time);
		$this->timeout = min(32767, max(1800, 300>$time?$time*60:$time));
	}

	#
	# Session functions
	#
	final public function destroy()
	{
		if (!self::$started) { return false; }
		$_SESSION = array();
		$K = \Dragonfly::getKernel();
		if ($K) {
			if (isset($_COOKIE[session_name()])) {
				$K->setCookie(session_name(), '', -1);
				unset($_COOKIE[session_name()]);
			}
			if ($K->SQL && isset($K->SQL->TBL->sessions)) {
				$K->SQL->query('DELETE FROM '.$K->SQL->TBL->sessions.' WHERE sess_id='.$K->SQL->quote(session_id()));
			}
		}
		self::$started = false;
		return session_destroy();
	}

	public function write_close(){ $this->close(); } // old code compatibility
	final public function close()
	{
		if (!self::$started) { return; }
		try {
			$_SESSION['_PID'] = self::pid();
			$this->triggerEvent('beforeWriteClose');
			$this->before_write_close();

			global $CPG_SESS, $module_name;
			if (defined('ADMIN_PAGES') && (!isset($_SERVER['REDIRECT_STATUS']) || 404 != $_SERVER['REDIRECT_STATUS'])) {
				$_SESSION['SECURITY']['page'] = $module_name;
				$CPG_SESS['admin']['page'] = $_GET->txt('op') ?: $_POST->txt('op');
			}
			$CPG_SESS['user']['page'] = $module_name;
			$CPG_SESS['user']['file'] = $_GET->txt('file') ?: $_POST->txt('file');
			$CPG_SESS['user']['uri'] = \URL::uri();
			if (isset($CPG_SESS['user']['redirect']) && $CPG_SESS['user']['redirect'] != $CPG_SESS['user']['uri'] && $module_name != 'Your_Account') {
				unset($CPG_SESS['user']['redirect']);
			}
			$_SESSION['CPG_SESS'] = $CPG_SESS;

			session_write_close();
		} catch (Exception $e) {
			error_log(__CLASS__ . ' ' . $e->getMessage()."\n".$e->getTraceAsString());
		}
		self::$started = false;
	}

	public function db_write($id, $value='')
	{
		$K = \Dragonfly::getKernel();
		if (!$K || !$K->SQL || !isset($K->SQL->TBL->sessions)) { return false; }
		$SQL = $K->SQL;
		$row = array(
			'sess_expiry' => time() + $this->timeout,
			'sess_ip'     => $SQL->quote($this->client_ip),
			'sess_value'  => $SQL->quoteBinary($value),
			'identity_id' => $SQL->quote($K->IDENTITY->id),
			'sess_uri'    => $SQL->quote(\URL::uri()),
		);
		if (self::is_new())
		{
			$row['sess_id']      = $SQL->quote($id);
			$row['sess_reset']   = 0;
			$row['sess_time']    = time();
			$row['sess_timeout'] = $this->timeout;
			$row['sess_user_agent'] = $SQL->quote($_SERVER['HTTP_USER_AGENT']);
			return $SQL->insertPrepared('sessions', $row);
		}
		return $SQL->updatePrepared('sessions', $row, 'sess_id='.$SQL->quote($id));
	}

	# garbage collector
	public function db_gc($maxlifetime=0)
	{
		$K = \Dragonfly::getKernel();
		if ($K && $K->SQL && isset($K->SQL->TBL->sessions)) {
			$K->SQL->exec('DELETE FROM '.$K->SQL->TBL->sessions.' WHERE sess_expiry < '.time());
			$K->SQL->exec('DELETE QUICK FROM '.$K->SQL->TBL->session.' WHERE time < '. (time() - DF_SESSION_FREQ_CLEAR_DB));
		}
		return true;
	}

	private function start($id=null)
	{
		if (self::$started) { $this->destroy(); }
		$this->setHandler(); # http://bugs.php.net/bug.php?id=32330
		session_id($id?$id:sha1(session_name().microtime()));
		if (self::$started = session_start()) {
			# calls save_handler read method
			$this->after_start(session_id());
		} else {
			cpg_error('Session start failed');
		}
	}

	# Create Protection ID
	protected static function pid()
	{
		static $pid;
		if (!$pid)
		{
			// Can't use all $_SERVER['HTTP_*'] vars because MSIE changes them randomly
			// Firefox changes when FirePHP/0.6 is active or not
			// So we only use a partial string and the stable ua detect object
			$pid = md5(
				substr($_SERVER['HTTP_USER_AGENT'],0,strpos($_SERVER['HTTP_USER_AGENT'],')'))
				.json_encode(\Poodle\UserAgent::detect())
			);
		}
		return $pid;
	}

	public function init_info()
	{
		global $CPG_SESS;
		$SQL = \Dragonfly::getKernel()->SQL;
		$ID = \Dragonfly::getKernel()->IDENTITY;
/*
		Only this file:
		- session_start stores when member started to view website
		- user_session_time store current visit
		BB :
		- user_lastvisit stores previous session end
		  recieved from $ID->session_time
		$CPG_SESS =  $_SESSION['CPG_SESS']
*/
//		if (!isset($CPG_SESS['session_start']) || $CPG_SESS['session_start'] < (time() - ($this->sess_time*60))) {
		if (!isset($CPG_SESS['session_start'])) {
			if ($ID->id > 1) {
				$ID->lastvisit = ($ID->session_time > 0) ? $ID->session_time : time();
				$SQL->query('UPDATE '.$SQL->TBL->users.' SET user_session_time='.time().', user_lastvisit='.$ID->lastvisit
					.' WHERE user_id=' . $ID->id);
			}
			$CPG_SESS['session_start'] = $CPG_SESS['session_time'] = time();
			$this->db_gc();
			$this->dbupdate = true;
		} else if (DF_SESSION_FREQ_UPDATE_DB < (time() - $CPG_SESS['session_time'])) {
			$CPG_SESS['session_time'] = time();
			if ($ID->id > 1) {
				list($user_level, $new, $unread) = $SQL->uFetchRow('SELECT user_level, user_new_privmsg, user_unread_privmsg FROM '.$SQL->TBL->users.' WHERE user_id='.$ID->id);
				if ($user_level < 1) {
					$this->destroy();
					\URL::redirect();
				}
				$ID->new_privmsg = intval($new);
				$ID->unread_privmsg = intval($unread);
				$SQL->query('UPDATE '.$SQL->TBL->users.' SET user_session_time='.time().' WHERE user_id=' . $ID->id);
			}
			$this->db_gc();
			$this->dbupdate = true;
		}
	}

}
